﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WordNet.Core.ElementContracts;
using WordNet.Core.Graph;
using WordNet.Core.Relations;
using WordNet.Core.Util;

namespace WordNet.Core
{
    //Calculates the degree to which two nouns are related
    public class SemanticCalculator: ISemanticRelatedness
    {
        IWNDictionary _dictionary;
        public IASPath _aspath;
        public SemanticCalculator(IWNDictionary dic)
        {
            if (dic == null)
                throw new ArgumentNullException(Message.NullException);

            _dictionary = dic;
            var graph = BuildGraph();
            ValidateGraph(graph);
            _aspath = new ASPath(graph);
        }
        
        public IEnumerable<string> Nouns()
        {
            return _dictionary.GetAllIndexWords(POS.NOUN).Select(w => w.Lemma);
        }
        
        public bool IsNoun(string word)
        {
            if (word == null)
                throw new ArgumentNullException(Message.NullException);
            
            return _dictionary.GetIndexWord(word, POS.NOUN) != null;
        }

        // distance is the minimum length of any ancestral path 
        // between any synset v of A and any synset w of B
        public int Distance(string nounA, string nounB)
        {
            if (nounA == null || nounB == null)
                throw new ArgumentNullException(Message.NullException);
            if (!IsNoun(nounA) || !IsNoun(nounB))
                throw new ArgumentNullException(Message.NotInDictionary);

            IEnumerable<int> syna = _dictionary.GetIndexWord(nounA, POS.NOUN).
                                     GetWordIDs().Select(w => w.SynsetID.Offset);
            IEnumerable<int> synb = _dictionary.GetIndexWord(nounB, POS.NOUN).
                                     GetWordIDs().Select(w=> w.SynsetID.Offset);
            
            return _aspath.Length(syna, synb);
        }

        // Finds the shortest ancestral path between the two vertices 
        // and a common ancestor that participates in that path:
        public ISynset CommonAcestor(string nounA, string nounB)
        {
            if (nounA == null || nounB == null)
                throw new ArgumentNullException(Message.NullException);
            if (!IsNoun(nounA) || !IsNoun(nounB))
                throw new ArgumentNullException(Message.NotInDictionary);

            var w1 = _dictionary.GetIndexWord(nounA, POS.NOUN);
            var w2 = _dictionary.GetIndexWord(nounB, POS.NOUN);
            var syn1 = w1.GetWordIDs().Select(w => w.SynsetID.Offset);
            var syn2 = w2.GetWordIDs().Select(w => w.SynsetID.Offset);
            var id = _aspath.Ancestor(syn1, syn2);
           
            return _dictionary.GetSynset(new SynsetID(id, POS.NOUN));
        }

        // Given a list of wordnet nouns A1, A2, ..., An, finds which noun is 
        // the least related to the others.
        public string FindLeastRelated(string[] nouns)
        {
            int[,] matrix = new int[nouns.Length, nouns.Length];
            int max = 0;
            string outcast = string.Empty;
            for (int i = 0; i < nouns.Length; i++)
            {
                int current = 0;
                for (int j = i; j < nouns.Length; j++)
                {
                    if (i != j)
                        current = Distance(nouns[i], nouns[j]);
                    matrix[i, j] = j > 0 ? matrix[i, j - 1] + current : current;
                    matrix[j, i] = i > 0 ? matrix[j, i - 1] + current : current;
                }
                if (matrix[i, nouns.Length - 1] > max)
                {
                    max = matrix[i, nouns.Length - 1];
                    outcast = nouns[i];
                }
            }
            return outcast;
        }

        private IDigraph BuildGraph()
        {
            const int capacity = 82192;

            IDigraph graph = new Digraph(capacity);
            foreach (var line in _dictionary.GetAllSynsets(POS.NOUN))
            {
               var hypernyms = line.GetRelatedSynsets(Pointer.HYPERNYM);
               if (hypernyms == null)
               {
                   hypernyms = line.GetRelatedSynsets(Pointer.HYPERNYM_INSTANCE);
                   if(hypernyms == null)
                        continue;
               }
               var id = line.ID.Offset;
               foreach (var h in hypernyms)
               {
                   graph.AddEdge(id, h.Offset);
               }
            }
            return graph;
        }
        
        private void ValidateGraph(IDigraph graph)
        {
            DirectedCycle dc = new DirectedCycle(graph);
            if (dc.HasCycle)
                throw new ArgumentException(Message.GraphWithCycle);

            if (!HasOneRoot(graph))
                throw new ArgumentException(Message.NotOneRoot);
       
        }

        private bool HasOneRoot(IDigraph graph)
        {
            int count = 0;
            foreach (var item in graph.GetAllVertices())
            {
                if (graph.Outdegree(item) == 0)
                    count++;
            }

            return count == 1;
        }
    }
}
